/**   
 * Copyright 2011 COMERE Harold
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package etrl.core;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;

import com.google.gwt.dom.client.Document;
import com.google.gwt.event.dom.client.ContextMenuEvent;
import com.google.gwt.event.dom.client.ContextMenuHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseOutEvent;
import com.google.gwt.event.dom.client.MouseOutHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.dom.client.MouseWheelEvent;
import com.google.gwt.event.dom.client.MouseWheelHandler;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Widget;
import com.googlecode.gwtgl.binding.WebGLCanvas;
import com.googlecode.gwtgl.binding.WebGLRenderingContext;

import etrl.camera.Camera;
import etrl.geometry.IntersectionDistanceComparator;
import etrl.geometry.LineSegmentIntersector;
import etrl.resources.Resources;
import etrl.shaders.ShaderProgramStore;
import etrl.vecmath.Matrix4f;
import etrl.vecmath.Vector4f;

/**
 * \class Viewer
 * \brief The viewer managing the GL canvas, event dispatchment and scene rendering.
 * @author harold
 *
 */
public class Viewer extends Timer
{
	/**
	 * \enum IntersectionSort
	 * \brief Sorting applied on the found intersections.
	 * @author harold
	 *
	 */
	public enum IntersectionSort
	{
		NO_SORT,					/** Order on which intersections have been found. */
		CRESCENT_DISTANCE_SORT,		/** Intersections ordered crescently according to their distance with the ray start. */
		DESCENDING_DISTANCE_SORT	/** Intersections ordered descending according to their distance with the ray start. */
	}
	
	/**
	 * The GL canvas
	 */
	private WebGLCanvas _webGLCanvas = null;
	
	/**
	 * The GL context
	 */
	private WebGLRenderingContext _gl;
	
	/**
	 * Current viewport
	 */
	private Viewport _viewport;
	
	/**
	 * Current window matrix
	 */
	private Matrix4f _windowMatrix;
	
	/**
	 * List of all registered event listeners on which the viewer has to dispatch events.
	 */
	private ArrayList <EventListener> _eventListeners;
	
	/**
	 * The mouse handler listening only mouse events on the canvas
	 */
	private MouseHandler _mouseEventsOnCanvasHandler = null;
	
	/**
	 * The mouse and keyboard events listening to all events on the whole document
	 */
	private MouseAndKeyboardHandler _mouseAndKeyboardEventsOnDocument = null;
	
	/**
	 * Main camera
	 */
	private Camera _camera;
	
	/**
	 * Current camera manipulator
	 */
	private TransformManipulator _cameraManipulator = null;

	/**
	 * Current scene data. The render visitor will start from it.
	 */
	private Node _sceneData = null;
	
	/**
	 * Loading resources status
	 * While loading resources, the viewer refresh will be blocked
	 */
	private boolean _loadingResources = false;
	
	private static Viewer instance = null;
	
//	public static Viewer getInstance(){
//		if(instance == null){
//			instance = new Viewer(1200, 800,EventHandlingMode.MOUSE_EVENTS_ON_CANVAS);
//		}
//		
//		return instance;
//	}
	/**
	 * \enum EventHandlingMode
	 * \brief Preset event handling modes
	 * @author harold
	 *
	 */
	public enum EventHandlingMode
	{
		MOUSE_EVENTS_ON_CANVAS,					/** Only mouse events are listened and only in canvas bounds */
		MOUSE_AND_KEYBOARD_EVENTS_ON_DOCUMENT	/** Mouse and keyboard events are listened in the whole document area */
	}

	/**
	 * \class MouseHandler
	 * \brief Class handling mouse events on the viewer canvas and dispatch them to event listeners
	 * @author harold
	 *
	 */
	private class MouseHandler implements MouseDownHandler, MouseUpHandler, MouseMoveHandler, MouseWheelHandler, MouseOutHandler, ContextMenuHandler
	{
		private HandlerRegistration _mouseDownRegistration = null;
		private HandlerRegistration _mouseUpRegistration = null;
		private HandlerRegistration _mouseMoveRegistration = null;
		private HandlerRegistration _mouseWheelRegistration = null;
		private HandlerRegistration _mouseOutRegistration = null;
		private HandlerRegistration _contextMenuRegistration = null;
		
		/**
		 * \brief Sets the mouse handler as listenening the given canvas
		 * @param[in] canvas Canvas to listen.
		 */
		public void setOnCanvas(WebGLCanvas canvas)
		{
			// Delete previous registration
			unset();
						
			// And register new handlers to the given canvas
			_mouseDownRegistration = canvas.addMouseDownHandler(this);
			_mouseUpRegistration = canvas.addMouseUpHandler(this);
			_mouseMoveRegistration = canvas.addMouseMoveHandler(this);
			_mouseWheelRegistration = canvas.addMouseWheelHandler(this);
			_mouseOutRegistration = canvas.addMouseOutHandler(this);
			_contextMenuRegistration = canvas.addDomHandler(this, ContextMenuEvent.getType());
		}
		
		/**
		 * \brief Stops the mouse handler listening.
		 */
		public void unset()
		{
			if(_mouseDownRegistration != null) _mouseDownRegistration.removeHandler();
			if(_mouseUpRegistration != null) _mouseUpRegistration.removeHandler();
			if(_mouseMoveRegistration != null) _mouseMoveRegistration.removeHandler();
			if(_mouseWheelRegistration != null) _mouseWheelRegistration.removeHandler();
			if(_mouseOutRegistration != null) _mouseOutRegistration.removeHandler();
			if(_contextMenuRegistration != null) _contextMenuRegistration.removeHandler();
		}
		
		/**
		 * {@inheritDoc}
		 */
		
		public void onMouseDown(MouseDownEvent event)
		{
			for(Iterator <EventListener> it = _eventListeners.iterator(); it.hasNext();)
			{
				it.next().onMouseDown(event.getNativeEvent());
			}	
			
			event.preventDefault();
		}

		/**
		 * {@inheritDoc}
		 */
		
		public void onMouseMove(MouseMoveEvent event)
		{
			for(Iterator <EventListener> it = _eventListeners.iterator(); it.hasNext();)
			{
				it.next().onMouseMove(event.getNativeEvent());
			}	
			
			event.preventDefault();
		}

		/**
		 * {@inheritDoc}
		 */
		
		public void onMouseUp(MouseUpEvent event)
		{
			for(Iterator <EventListener> it = _eventListeners.iterator(); it.hasNext();)
			{
				it.next().onMouseUp(event.getNativeEvent());
			}
			
			event.preventDefault();
		}

		/**
		 * {@inheritDoc}
		 */
		
		public void onMouseWheel(MouseWheelEvent event)
		{
			for(Iterator <EventListener> it = _eventListeners.iterator(); it.hasNext();)
			{
				it.next().onMouseWheel(event.getNativeEvent());
			}
			
			event.preventDefault();
		}
		
		/**
		 * {@inheritDoc}
		 */
		
		public void onMouseOut(MouseOutEvent event)
		{
			for(Iterator <EventListener> it = _eventListeners.iterator(); it.hasNext();)
			{
				it.next().onMouseOut(event.getNativeEvent());
			}	
		}
		
		/**
		 * {@inheritDoc}
		 */
		
		public void onContextMenu(ContextMenuEvent event)
		{
			event.preventDefault();
		}
	};
	
	/**
	 * \class MouseAndKeyboardHandler
	 * \brief Class handling mouse and keyboard events on the whole document and dispatch them to event listeners
	 * @author harold
	 *
	 */
	private class MouseAndKeyboardHandler implements NativePreviewHandler, ContextMenuHandler
	{
		private HandlerRegistration _previewNativeEventRegistration = null;
		private HandlerRegistration _contextMenuRegistration = null;
		
		/**
		 * \brief Sets the handler as listenening the document events, and the context menu canvas event
		 * @param[in] canvas Canvas to listen.
		 */
		public void setOnCanvas(WebGLCanvas canvas)
		{
			// Delete previous registration
			unset();
			
			_previewNativeEventRegistration = Event.addNativePreviewHandler(this);
			_contextMenuRegistration = canvas.addDomHandler(this, ContextMenuEvent.getType());
		}
		
		/**
		 * \brief Stops the handler listening events.
		 */
		public void unset()
		{
			if(_previewNativeEventRegistration != null) _previewNativeEventRegistration.removeHandler();
			if(_contextMenuRegistration != null) _contextMenuRegistration.removeHandler();
		}
		
		/**
		 * {@inheritDoc}
		 */
		
		public void onPreviewNativeEvent(NativePreviewEvent event)
		{
			final int mouseScroll 	= 131072;
			final int keyUp			= 512;
			//final int keyPress		= 256;
			final int keyDown		= 128;
			final int mouseMove		= 64;
			//final int mouseOut		= 32;
			//final int mouseOver		= 16;
			final int mouseUp		= 8;
			final int mouseDown		= 4;
			//final int mouseClick	= 1;
			
			switch(event.getTypeInt())
			{
			case keyDown :
				for(Iterator <EventListener> it = _eventListeners.iterator(); it.hasNext();)
				{
					it.next().onKeyDown(event.getNativeEvent());
				}
				
				event.getNativeEvent().preventDefault();
				break;
				
			case keyUp :
				for(Iterator <EventListener> it = _eventListeners.iterator(); it.hasNext();)
				{
					it.next().onKeyUp(event.getNativeEvent());
				}
				
				event.getNativeEvent().preventDefault();
				break;
				
			case mouseDown :
				for(Iterator <EventListener> it = _eventListeners.iterator(); it.hasNext();)
				{
					it.next().onMouseDown(event.getNativeEvent());
				}
				
				event.getNativeEvent().preventDefault();
				break;
				
			case mouseUp :
				for(Iterator <EventListener> it = _eventListeners.iterator(); it.hasNext();)
				{
					it.next().onMouseUp(event.getNativeEvent());
				}
				
				event.getNativeEvent().preventDefault();
				break;
				
			case mouseMove :
				for(Iterator <EventListener> it = _eventListeners.iterator(); it.hasNext();)
				{
					it.next().onMouseMove(event.getNativeEvent());
				}
				
				event.getNativeEvent().preventDefault();
				break;
				
			case mouseScroll :
				for(Iterator <EventListener> it = _eventListeners.iterator(); it.hasNext();)
				{
					it.next().onMouseWheel(event.getNativeEvent());
				}
				
				event.getNativeEvent().preventDefault();
				break;
			}
		}

		/**
		 * {@inheritDoc}
		 */
		
		public void onContextMenu(ContextMenuEvent event)
		{
			event.preventDefault();
		}
		
	}
	
	/**
	 * \class CanvasResizeHandler
	 * \brief Handler to manage canvas resize event
	 * @author harold
	 *
	 */
	private class CanvasResizeHandler implements ResizeHandler
	{
		/**
		 * The viewer to manage.
		 */
		Viewer _viewer;
		
		/**
		 * \brief Initializes the handler
		 * @param[in] viewer
		 */
		public CanvasResizeHandler(Viewer viewer)
		{
			_viewer = viewer;
		}
		
		/**
		 * {@inheritDoc}
		 */
		
		public void onResize(ResizeEvent event)
		{
			_viewer.setViewerDims(Document.get().getClientWidth(), Document.get().getClientHeight());
		}
		
	}
	
	/**
	 * \brief Initialize the viewer using given dimensions and event handling mode
	 * @param[in] width Requested viewer width
	 * @param[in] height Requested viewer height
	 * @param[in] eventHandlingMode Requested event handling mode
	 */
	public Viewer(int width, int height, EventHandlingMode eventHandlingMode)
	{
		initViewer(width, height, eventHandlingMode);
	}
	
	/**
	 * \brief Initialize the viewer unsing the maximum size it cans use.
	 * @param eventHandlingMode
	 */
	public Viewer(EventHandlingMode eventHandlingMode)
	{
		// Init the viewer with the maximum size allowed by the document
		initViewer(Document.get().getClientWidth(), Document.get().getClientHeight(), eventHandlingMode);
		// And add the resize handler as the viewer size isnt fixed
		Window.addResizeHandler(new CanvasResizeHandler(this));
	}
	
	/**
	 * \brief Initializes the viewer.
	 * @param width Requested viewer width
	 * @param height Requested viewer height
	 * @param eventHandlingMode Requested event handling mode
	 */
	private void initViewer(int width, int height,  EventHandlingMode eventHandlingMode)
	{		
		try
		{
			// Create the main camera
			_camera = new Camera();
			
			// Set the requested viewer dimensions
			setViewerDims(width, height);
			
			// === TODO : Init gl context anywhere else to be able to share the same context between every viewers
			Engine.getInstance().setGL(_gl);
			// ===
			
			// Set the current context to Resources to be able to load textures in it
			Resources.instance().setGLContext(_gl);
			
			// Initialize the adequat event handler according to requested event handling mode
			if(eventHandlingMode == EventHandlingMode.MOUSE_EVENTS_ON_CANVAS)
			{
				_mouseEventsOnCanvasHandler = new MouseHandler();
				_mouseEventsOnCanvasHandler.setOnCanvas(_webGLCanvas);
				
			}
			
			if(eventHandlingMode == EventHandlingMode.MOUSE_AND_KEYBOARD_EVENTS_ON_DOCUMENT)
			{
				_mouseAndKeyboardEventsOnDocument = new MouseAndKeyboardHandler();
				_mouseAndKeyboardEventsOnDocument.setOnCanvas(_webGLCanvas);
			}
			
			// Initializes basic webgl parameters
			initGL();
		
			// And create the event listener array
			_eventListeners = new ArrayList <EventListener>();
		}
		catch (Exception e)
		{
			Window.alert("Sorry, your browser doesnt support WebGL :-(");
		}
	}
	
	public MouseHandler getMouseHandler(){
		return _mouseEventsOnCanvasHandler;
	}
	
	/**
	 * \brief Sets new viewer dimensions.
	 * @param width New viewer width to set.
	 * @param height New viewer height to set.
	 */
	private void setViewerDims(int width, int height)
	{
		// First build dimensions strings, needed by WebGLCanvas
		String widthString = String.valueOf(width) + "px";
		String heightString = String.valueOf(height) + "px";
		
		// If the canvas isnt instantiated yet
		if(_webGLCanvas == null)
		{
			// Instanciate it with requested dimensions
			_webGLCanvas = new WebGLCanvas(widthString, heightString);
			// And store the GL context
			_gl = _webGLCanvas.getGlContext();
		}
		else // Else the canvas is already instantiated
		{
			// So just modify the dimensions
			_webGLCanvas.setWidth(widthString);
			_webGLCanvas.setHeight(heightString);
		}

		// Create the viewport
		_viewport = new Viewport(0, 0, width, height);
		
		// Set viewport dimensions for the GL context
		_gl.viewport(0, 0, width, height);
		
		// Compute corresponding window matrix ( includes bias matrix )
		_windowMatrix = new Matrix4f(
			width * 0.5f, 	0.f, 			0.f, 	width * 0.5f,
			0.f,  			height * 0.5f, 	0.f, 	height * 0.5f,
			0.f,			0.f,			0.5f, 	0.5f,
			0.f,			0.f,			0.f,	1.f
		);
		
		// And update the main camera projection matrix
		_camera.setProjectionAsPerspective(60.f, (float) width / (float) height, 0.1f, 1000.f);
	}
	
	/**
	 * \brief Register a new event listener.
	 * @param listener Event listener to register.
	 */
	public void registerListener(EventListener listener)
	{
		_eventListeners.add(listener);
	}
	
	/**
	 * \brief Unregister the given event listener, it wont receives events anymore.
	 * @param listener Event listener to unregister.
	 */
	public void unregisterListener(EventListener listener)
	{
		if(! _eventListeners.remove(listener))
		{
			Window.alert("[etrl.viewer.Viewer::unregisterListener] Unexpected error, the given listener is not registered");
		}
	}
	
	/**
	 * \brief Initializes some basic GL parameters
	 */
	private void initGL()
	{
		_gl.clearColor(0.9f, 0.9f, 0.9f, 1.0f);
		_gl.clearDepth(1.0f);
		_gl.enable(WebGLRenderingContext.DEPTH_TEST);
		_gl.depthFunc(WebGLRenderingContext.LEQUAL);
	}
	
	/**
	 * \brief Gets the main camera
	 * @return Main camera
	 */
	public Camera getCamera()
	{
		return _camera;
	}
	
	/**
	 * \brief Gets the GL canvas as Widget and so can be set in any gwt layout.
	 * @return Canvas widget.
	 */
	public Widget getCanvas()
	{
		return _webGLCanvas;
	}
	
	public WebGLCanvas getWebCanvas(){
		return _webGLCanvas;
	}
	
	public WebGLRenderingContext getWebGLRenderingContext(){
		return _gl;
	}
	/**
	 * \brief Process to do at frame event reception
	 */
	public void onFrameEvent()
	{
		// Just dispatch it to event listeners
		for(Iterator <EventListener> it = _eventListeners.iterator(); it.hasNext();)
		{
			it.next().onFrameEvent();
		}
	}
	
	/**
	 * \brief Refresh the viewer and launch the scene rendering.
	 */
	public void frame()
	{	
		// If not loading resources, proceed to display loop
		if(!_loadingResources)
		{
			// Dispatch the frame event to every listener befor to statd drawing anything
			onFrameEvent();
			
			// Update the camera position and orientation according to the camera manipulator
			if(_cameraManipulator != null)
			{
				_camera.setPosition(_cameraManipulator.getTransformPosition());
				_camera.setOrientation(_cameraManipulator.getTransformOrientation());
			}
			
			_gl.clear(WebGLRenderingContext.COLOR_BUFFER_BIT | WebGLRenderingContext.DEPTH_BUFFER_BIT);
		
			// Launch the render visitor to render scene, from the main camera
			RenderVisitor renderVisitor = new RenderVisitor();
			_camera.accept(renderVisitor);
		}
		else // else resources are loading
		{
			// so check if it is terminated
			if(Resources.instance().checkResourceLoad() == 0)
				_loadingResources = false;
		}
	}
	
	/**
	 * \brief Gets the current viewport
	 * @return Viewport
	 */
	public Viewport getViewport()
	{
		return _viewport;
	}

	/**
	 * \brief Defines the used camera manipulator.
	 * @param[in] manipulator Camera manipulator to set.
	 */
	public void setCameraManipulator(TransformManipulator manipulator)
	{
		// First unset the current manipulator if there is one
		if(_cameraManipulator != null) unregisterListener(_cameraManipulator);
		
		_cameraManipulator = manipulator;
		registerListener(manipulator);
	}
	
	/**
	 * \brief Defines the scene data to render.
	 * @param[in] sceneData Scene data to set.
	 */
	public void setSceneData(Node sceneData)
	{
		// First remove the scene data as child of the viewer camera
		_camera.removeChilds();
		
		// Define the current scene data
		_sceneData = sceneData;
		
		// Then add the new scene data as camera child
		_camera.addChild(_sceneData);
		
		// As we need at least the default shader to be able to draw anything,
		// if the given sceneData has no linked shader program, link the default one.
		// This will ensure that the default shader will be used on the whole childwood if no shader program is defined at all.
		if(_sceneData.getShaderProgram() == null)
		{
			_sceneData.setShaderProgram(ShaderProgramStore.getInstance().getShaderProgram("DefaultShader"));
		}	
	}
	
	public Node getScene(){
		return _sceneData;
	}
	
	/**
	 * \brief Gets the current window matrix.
	 * @return Window matrix.
	 */
	public Matrix4f getWindowMatrix()
	{
		return new Matrix4f(_windowMatrix);
	}
	
	/**
	 * \brief Launch a ray from mouse position and gets the intersections list with the scene geometry
	 * @param[in] x X coordinate of the mouse
	 * @param[in] y Y coordinate of the mouse
	 * @param[out] start Computed 3D start point of the ray
	 * @param[out] end Computed 3D end point of the ray
	 * @param[in] intersectionSort Requested intersection sorting mode
	 * @return Sorted intersection list.
	 */
	public ArrayList <LineSegmentIntersector.Intersection> intersects(int x, int y, Vector4f start, Vector4f end, Viewer.IntersectionSort intersectionSort)
	{
		// If scene data is not defined, there is nothing to intersect
		if(_sceneData != null)
		{
			// Well screen coordinates clipping bounds are 
			//		x -> [0.,viewport.width] 
			//		y -> [0.,viewport.height]
			//		z -> [0., 1.] 
			// with origin as top left corner.
			//
			// The opengl convention set origin as bottom left corner so we have to do viewport.height - y for y coordinate.
			// z = 0. means the point is on the near plane and thats typically where we want our ray starts.
			start.x = (float) x;
			start.y = _viewport.getValues()[3] - y;
			start.z = 0.f;
			start.w = 1.f;
			
			// For the end point we want the other side of the view volume.
			// So z = 1.f
			end.x = start.x;
			end.y = start.y;
			end.z = 1.f;
			end.w = 1.f;
			
	
			Matrix4f windowMatrix = new Matrix4f(_windowMatrix);
			windowMatrix.invert();
			windowMatrix.transform(start);
			windowMatrix.transform(end);
	
			// Simulate perspective division iverse
			start.scale(_camera.getZNear());
			end.scale(_camera.getZFar());
			
			Matrix4f projectionMatrix = _camera.getProjectionMatrix();
			projectionMatrix.transpose();
			projectionMatrix.invert();
			projectionMatrix.transform(start);
			projectionMatrix.transform(end);
			
			_camera.getViewMatrix().transform(start);
			_camera.getViewMatrix().transform(end);
			
			//System.out.println("==== LineSegmentIntersector gogogo ===");
			LineSegmentIntersector intersector = new LineSegmentIntersector(start, end);
			_sceneData.accept(intersector);
		
			ArrayList <LineSegmentIntersector.Intersection> intersections = intersector.getIntersections();
				
			switch(intersectionSort)
			{
			case NO_SORT :
				// Nothing to do
				break;
					
			case CRESCENT_DISTANCE_SORT :
				// Sort by crescent distance order
				Collections.sort(intersections, new IntersectionDistanceComparator(start, false));
				break;
					
			case DESCENDING_DISTANCE_SORT :
				// Sort by descending distance order
				Collections.sort(intersections, new IntersectionDistanceComparator(start, true));
				break;
			}
				
			return intersections;
		}
		else
		{
			// no scene data, return empty arraylist
			return new ArrayList <LineSegmentIntersector.Intersection>();
		}
	}
	
	/**
	 * \brief Launch the resources loading and display progress bar waiting the loading end.
	 */
	public void loadResources()
	{
		Resources.instance().loadResources();
		_loadingResources = true;
	}

	/**
	 * \brief Clear all currently loaded resources
	 */
	public void unloadResources()
	{
		Resources.instance().clearResources();
	}

	public void run() {
		this.frame();
	}
}
